在之前的章節,我們看過了狀態組件和無狀態組件的差異以及利用 setState
重新呼叫 build
方法。然而狀態組件還有一些額外的生命週期需要了解,因為它們對於管理輸入資料非常重要。隨著我們探討更進階的組件互動,這些生命週期也會逐漸變得更加重要。
狀態組件會經歷一些關鍵生命週期狀態。我們將會探討一些大部分情況會需要的狀態,後續我們介紹一些在特定情境和案例下所需的生命週期狀態。
狀態的建立是發生在 StatefulWidget
生命週期最開始的階段,就在建構子呼叫之後。這個組件會通過呼叫 createState()
方法並回傳一個搭配的狀態物件,例如 State<MyWidget> _MyWidgetState
,也就是 State
物件。這是生命週期中必不可少的步驟,,少了這步,狀態組件就無法擁有狀態。
下面是 createState
的範例:
// ⚠️ 注意部分舊版的程式可能會被 analysis_options 的規範警告
class MyHomePage extends StatefulWidget {
const MyHomePage({ super.key, this.title });
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
// ...
}
範例展示了如何定義一個狀態組件 (StatefulWidget
) 和其對應狀態 (State
) 的基本框架。接著讓我們來改寫 DestinationWidget
成為狀態組件:
import 'package:flutter/material.dart';
class DestinationWidget extends StatefulWidget {
const DestinationWidget({super.key, required this.destinationName});
final String destinationName;
@override
State<DestinationWidget> createState() => _DestinationWidgetState();
}
class _DestinationWidgetState extends State<DestinationWidget> {
@override
Widget build(BuildContext context) {
return Text(widget.destinationName);
}
}
範例中 DestinationWidget
使用了 createState()
並回傳了一個狀態物件,而狀態物件則包含了本來在 StatelessWidget
中的 build
。也就是現在介面搬到了狀態物件中來建立。不過這也表示要取得 destinationName
現在我們需要使用引用 widget.destinationName
。State
物件可以使用 widget
來取得搭配的 StatefulWidget
物件參考。
為什麼 DestinationWidget 類別內部又可以使用自己
State<DestinationWidget>
?簡單的說,編譯器在掃描階段就知道了類別的存在,因此可以在
createState()
中使用。類別具體的內容則會在後續的階段解析。且 Dart 具備延遲解析,另外,Dart 允許某些形式的循環引用。例如,類別 A 可以引用類別 B,而類別 B 也可以引用類別 A,這些循環引用在 Dart 中是允許的,前提是編譯器在解析時可以找到所有引用的定義。
State
的物件實例在 initState()
方法初始化了它的狀態變數和其他基礎條件,例如資料庫連線。這個方法只會在當 Widget 第一次被加入樹狀結構的時候被呼叫一次,然後其呈現在使用者面前,並且這個方法是可選的。
我們後續將會探討一些例子,但 initState()
的基本例子如下:
@override
void initState() {
super.initState();
}
可以看到這個方法的第一行必須要初始化 super
類別,然後接著你自訂的邏輯初始化狀態。對於我們的 DestinationWidget
我們可以從資料庫讀取資料初始化計數器,範例為了單純我們直接設定 0
class _DestinationWidgetState extends State<DestinationWidget> {
late int _counter;
@override
void initState() {
super.initState();
// 可以從資料庫讀取資料來初始化
_counter = 0;
}
@override
Widget build(BuildContext context) {
return Text(widget.destinationName);
}
}
注意到我們必須使用 late
將 _counter
宣告為延遲初始化變數,這是因為它是在建構子執行之後才進行的。然而它會確保在我們使用它之前就別設置,因此不需要使用 null
。
補充:基於 Dart Null-Safe 空安全的特性,一但有可能為
null
我們通常要附加宣告例如int? num
。但一旦使用了null
後續又會增加很多判斷和限制。因此在一些情況,例如我們可以確保在某變數被使用之前一定會賦值,這時我們可以使用late
延遲初始化。關鍵就是在開發時,要儘量減少可 null 的變數。
- 一般宣告:必須在宣告時或建構子內完成初始化
- 可 null 變數:不受限制
late
:介於一般宣告和可 null 之間,即初始化可以被延遲,保證在該變數被使用之前完成初始化關鍵字複習:
final
: 定義變數只能被賦值一次,變數的值不需要在編譯時期就固定,可以在執行時初始化,一但賦值就不能改變const
:宣告常數,編譯時期就固定,且不能再被修改。final
和const
的主要區別在於:final
允許在執行時初始化,而const
要求在編譯時期就確定值。var
:宣告變數,隱式宣告讓 Dart 自行推斷型別,後續可被修改late
:延遲一個不為空的變數,確保變數會在第一次使用之前就完成初始化。是一種新的關鍵字,在 Dart 2.12 中引入,可以幫助避免在初始化不為空變數時出現空引用錯誤。async
:標記非同步,可搭配await
等待完成操作await
:等待非同步完成操作總結來說實務上,固定的值使用
const
,例如從伺服器讀取的設定可以使用final
存放不可變的設定值,用於執行時期才能確定,但之後不會改變的值。late
使用在初始化之後確定有值的情況。
當 Widget 要渲染到畫面時會呼叫 build
方法。這個方法會在 initState
和每一次呼叫 setState
之後呼叫。讓我們變更 DestinationWidget
加入按鈕和操作行為:
@override
Widget build(BuildContext context) {
return Row(
children: [
Text(widget.destinationName),
Column(
children: [
IconButton(
onPressed: () {
setState(() {
_counter++;
});
},
icon: const Icon(Icons.thumb_up),
),
Text(_counter.toString()),
],
),
],
);
}
雖然乍看之下很多程式碼,但如果你分別關注每個 Widget,會發現所有的元素和知識你已經都理解了。
首先,我們使用了 Row
,裡面包含了兩個組件:Text
和 Column
。Row
會水平排列包含目的地名稱的文字和 Column
。而 Column
也有兩個組件:IconButton
和顯示讚次數的文字,兩者會垂直排列。
最後,IconButton
使用了 onPressed
參數其對應了點擊後的行為,在這個行為中我們使用了 setState
變更了狀態變數,隨後會如我們上面提到的, Flutter 會重新呼叫 build
刷新畫面。
另外 Flutter 內建支援 Material Design 風格的 Icon 組件,上面例子使用了 thumb_up
。Icon 名稱列表
在專案中使用預設的 Material Design Icon 須在
pubspec.yaml
設定uses-material-design: true
Flutter 中Icon
是顯示圖示的 Widget。原理是使用字體圖示,類似於 Web 早期的 FontAwesome 使用字體搭配 Unicode 來實現圖示,字體圖示是一種將本來應該顯示的文字換成圖示的實現方式。(主流的圖示框架現多採用 SVG 的方式實現)。Icon
的範例如下:Icon( Icons.thumb_up, color: Colors.pink, size: 24.0, semanticLabel: '語意化文字' )
另外,如果要使用其他字體的方式也可以使用 IconData。先設定載入字體:
flutter: fonts: - family: YourIconFontFamily fonts: - asset: assets/fonts/YourIconFont.ttf
使用
IconData
:Icon(IconData(0xe800, fontFamily: 'YourIconFontFamily'))
當一個組件從我們的樹狀結構移除的時候會呼叫 dispose()
方法。這個方法通常用於清理 initState()
期間建立所需的基本條件例如:監聽事件,資料庫、網路連線等等。我們會在 dispose()
這個方法解除這些行為。
@override
void dispose() {
// 清理,解除的程式在這。
super.dispose();
}
這次 super.dispose()
在最後一行,其他所需的邏輯則是在前面處理。
一般常見的錯誤來源就是在 dispose
未能關閉一些連接,如果沒有關閉一些連線和監聽事件仍會繼續執行嘗試和組件互動進而耗費資源例如記憶體。如果有一個未正確關閉的連線嘗試對已不存在結構中的組件呼叫 setState
將會看到錯誤提示。
除了生命週期狀態,還有一個很重要的屬性叫 mounted
。這個屬性是用來檢查組件是否仍然掛載在樹狀結構上。具體來說,當一個 State
物件建立且呼叫 initState
之前,Flutter 會通過和BuildContext
關聯掛載這個物件,也就是 initState
呼叫時,mounted
就會被標記為 true
。而 dispose
時則變成 false
。
除非 mounted
為 true
否則呼叫 setState
就會發生錯誤。
在實務上,例如在監聽資料庫或網路連接的情況下,我們會用這個屬性來檢查。如果資料庫或網路連線狀態的變化觸發組件更新,那麼後續在呼叫 setState
之前加入一個 mounted
檢查會更加保險。因為有可能在組件移除的過程收到事件。一個簡單的範例如下:
if (mounted) {
setState(() {
// 更新狀態的操作
});
}
通過 mounted
我們可以確保組件仍然還在。到此我們了解了關於組件一些重要的生命週期,也具備足夠的知識處理表單欄位組件的手勢了。
除了上面實務上比較常用的生命週期之外,另外還有:
didChangeDependencies()
:這個方法在 initState
執行後立即被呼叫,也會在 State 物件的依賴關係通過 InheritedWidget 發生變化時被呼叫。didUpdateWidget()
當組件更新了新的屬性時呼叫執行,常見的例子就由上層組件通過建構子向子組件傳遞某些變數時。deactivate()
當使用 GlobalKey
將 State
將組件從一個位置(A 樹狀結構下)移動到另一個位置(B 樹狀結構下)的過程中被調用。關於生命週期更多的資訊請參考官方文件。